/**
 * 
 */
package monitor;

import java.util.ArrayList;
import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;


/**
 * @author Robbie
 *
 */
public class PacketSender extends Thread implements PropertyChangeListener {
	
	private static int MAX_PACKET_SIZE = 1500;
	
	private ArrayList<DatagramPacket> packetQueue;
	private ArrayList<RemoteHost> nodeList;
	private ArrayList<RemoteHost> myTrackers;
	private int timerDelay;
	private Timer myTimer;
	private TimerTask myTask;
	private DatagramSocket mySocket;
	private boolean isReady;
	private UUID myUUID;
	
	private PropertyChangeSupport listeners = new PropertyChangeSupport(this);
	
	public PacketSender(RemoteHost tracker, int delay){
		myTrackers = new ArrayList<RemoteHost>();
		
		try {
			mySocket = new DatagramSocket();
		} catch (SocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		packetQueue = new ArrayList<DatagramPacket>();
		nodeList = new ArrayList<RemoteHost>();
		timerDelay = delay;
		myTimer = new Timer();
		myTrackers.add(tracker);
		isReady = false;
		myUUID = UUID.randomUUID();
		setTimerTask();
	}

	/* (non-Javadoc)
	 * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
	 */
	public void propertyChange(PropertyChangeEvent evt) {
		String name = evt.getPropertyName();
		if(name.equals("PACKET")){
			byte[] buffer = ((String)evt.getNewValue()).getBytes();
			String length = new String(buffer,8,4);
			if(Integer.parseInt(length,16) == (buffer.length-12)){
				DatagramPacket pkt = new DatagramPacket(buffer, buffer.length);
				synchronized(packetQueue){
					packetQueue.add(pkt);
				}
			}else {
				try {
					throw new Exception("Bad packet from "+evt.getSource().getClass());
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} else if(name.equals("NEW NODE")){
//			System.out.println("PS recieved new node!");
			byte[] buffer = (byte[]) evt.getNewValue();
			String uuid = ((RemoteHost) evt.getOldValue()).getUUID().toString();
			String node = new String(buffer);
			int split = node.indexOf(":");
			InetAddress ipAddr = null;
			int port;
			if(split>0){
				try {
					ipAddr = InetAddress.getByName(node.substring(0, split));
				} catch (UnknownHostException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				if(ipAddr == null){
					return;
				}
				port = Integer.parseInt(node.substring(split+1,node.length()));
			}else{
				ipAddr = ((RemoteHost) evt.getOldValue()).getAddress();
				port = Integer.parseInt(node);
			}
			RemoteHost h = new RemoteHost(ipAddr,port,uuid);
			if(!isNodeInList(h)){
//				System.out.println("PS creating RemoteHost with UUID: "+h.getUUID());
				synchronized(nodeList){
					nodeList.add(h);
				}
				PropertyChangeEvent e = new PropertyChangeEvent(this, "NEWNODE", null, h);
				scheduleUpdate(e);
			}	
		}
	}
	
	/**
	 * @param ip
	 * @param port
	 * @return
	 */
	private boolean isNodeInList(RemoteHost h){
		ArrayList<RemoteHost> nodes = new ArrayList<RemoteHost>();
		synchronized(nodeList){
			nodes.addAll(nodeList);
		}
		for(int loop=0;loop<nodes.size();loop++){
			if(nodes.get(loop).equals(h)){
				return true;
			}
			if(nodes.get(loop).equals(h.getAddress(), h.getPort())){
				synchronized(nodeList){
					nodeList.remove(nodes.get(loop));
					nodeList.add(h);
				}
				PropertyChangeEvent e = new PropertyChangeEvent(this,"REMOVE_NODE",null,nodes.get(loop).getUUID());
				scheduleUpdate(e);
			}
		}
		return false;
	}
	
	public void run(){
//		System.out.println("Packet sender started");
		if(!isReady){
			try {
				throw new Exception("Packet sender is not ready!");
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
		
		myTimer.scheduleAtFixedRate(myTask, 0, timerDelay);
		
	}
	
	public void setup(int localListeningPort){
		if(isReady){
			return;
		}
//		System.out.println("Begun setup of PS...");
		String pkt = new String("NEW NODE");
		String receivingPort = Integer.toString(localListeningPort);
		String length = Integer.toString(receivingPort.length(), 16);
		if(length.length()==1){length = "000" + length;}
		else if(length.length()==2){length = "00" + length;}
		else if(length.length()==3){length = "0" + length;}
		pkt = myUUID.toString() + pkt + length + receivingPort;
		DatagramPacket packet = new DatagramPacket(pkt.getBytes(),pkt.length());
		for(int loop = 0; loop<myTrackers.size();loop++){
			packet.setAddress(myTrackers.get(loop).getAddress());
			packet.setPort(myTrackers.get(loop).getPort());			
			try {
				mySocket.send(packet);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}	
		}
		pkt = "NEW NODE" + length + receivingPort;
		packet = new DatagramPacket(pkt.getBytes(),pkt.length());
		synchronized(packetQueue){packetQueue.add(packet);} //packets to introduce New Node to Existing Nodes
		isReady = true;
//		System.out.println("Setup of PS finished");
	}
	
	/**
	 * 
	 */
	private void setTimerTask(){
		myTask = new TimerTask(){
			
			@Override
			public void run() {
//				System.out.println("Timertask run");
				ArrayList<DatagramPacket> queue = new ArrayList<DatagramPacket>();
				ArrayList<RemoteHost> hosts = new ArrayList<RemoteHost>();
				if(nodeList.size()<1){
					return;
				}
				synchronized(packetQueue){
					queue.addAll(packetQueue);
					packetQueue = new ArrayList<DatagramPacket>();
				}
				synchronized(nodeList){
					hosts.addAll(nodeList);
				}
				if(queue.size()<1){
					return;
				}
				for(int node = 0; node < hosts.size(); node++){
//					System.out.println("Forming packets");
					int size;
					ArrayList<DatagramPacket> formedPackets = new ArrayList<DatagramPacket>();
					for(int loop=0;loop<queue.size();loop++){
						size = 0;
						String data = myUUID.toString();
						String chunk = new String(queue.get(loop).getData());
						for(;loop<queue.size() && (size+chunk.length())<MAX_PACKET_SIZE;loop++){
							int temp = data.length();
							data += chunk;
							size += data.length()-temp;
							chunk = new String(queue.get(loop).getData());
						}
						formedPackets.add(new DatagramPacket(data.getBytes(),data.length()));
					}
					while(!formedPackets.isEmpty()){
						DatagramPacket packet = formedPackets.remove(0);
//						System.out.println("PS sending this packet: "+new String(packet.getData()));
						packet.setAddress(hosts.get(node).getAddress());
						packet.setPort(hosts.get(node).getPort());
						try {
							mySocket.send(packet);
						} catch (IOException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}	//end of while loop
				}	//end of for loop
			}	//end of run()
		};
	}
	
	/**
	 * Adds a property change listener.  Only used to pass node data to PacketAnalyzer.
	 * 
	 * @param l listener to add
	 */
	public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
	    listeners.addPropertyChangeListener(l);
	}
	
	/**
	 * Removes a property change listener
	 * 
	 * @param l listener to remove
	 */
	public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
	    listeners.removePropertyChangeListener(l);
	}
	
	protected void fireUpdate(PropertyChangeEvent evt) {
	    listeners.firePropertyChange(evt);
	}


	protected void scheduleUpdate(final PropertyChangeEvent evt) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				fireUpdate(evt);
			}
		});
	}

}
